#  create Proxy with simple error behaviour :
# on error select another ip::port from pool and try to reconects up to $REPEAT times
package Net::Coro::Proxy;
use common::sense;
our $VERSION = 0.03;

use Net::Coro::EV::Connect::Socks5;
use Net::Coro::EV::Connect::Socks4;
use Net::Coro::EV::Connect::Proxy;
use Net::Coro::EV::Connect::HTTPProxy;
use Net::Coro::ProxyPool;

our $REPEAT = 5;
our $TRACE = 0;

sub new {
	my ($class, $socks5, $socks4, $proxy, $http_proxy, $socket) =@_;
	
	my $this = bless {}, $class;
	$this->{_TYPES} = [];
	if ($http_proxy) {
		$this->{_HTTPPROXY_POOL} = Net::Coro::ProxyPool->new($http_proxy);
		$this->{_HTTPPROXY} = Net::Coro::EV::Connect::HTTPProxy->new(undef, undef, $socket);
		push @{$this->{_TYPES}}, '_HTTPPROXY';
	}
	if ($proxy) {
		$this->{_PROXY_POOL} = Net::Coro::ProxyPool->new($proxy);
		$this->{_PROXY} = Net::Coro::EV::Connect::Proxy->new(undef, undef, $socket);
		push @{$this->{_TYPES}}, '_PROXY';
	}
	if ($socks5) {
		$this->{_SOCKS5_POOL} = Net::Coro::ProxyPool->new($socks5);
		$this->{_SOCKS5} = Net::Coro::EV::Connect::Socks5->new(undef, undef, $socket);
		push @{$this->{_TYPES}}, '_SOCKS5';
	}
	if ($socks4) {
		$this->{_SOCKS4_POOL} = Net::Coro::ProxyPool->new($socks4);
		$this->{_SOCKS4} = Net::Coro::EV::Connect::Socks4->new(undef, undef, $socket);
		push @{$this->{_TYPES}}, '_SOCKS4';
	}
	
	return if ! scalar @{$this->{_TYPES}};

	$this->{_REPEAT_LIMIT} = $REPEAT;
	$this->{_REPEAT} = $this->{_REPEAT_LIMIT};
	$this->id(rand()) if $TRACE;
	
	$this->assign_sock;
	return $this;
}

sub child {
	my ($this) = @_;
	my $child = {};
	bless $child, ref $this;
		
	while (my ($key, $val) = each(%$this)) {
		$child->{$key} = $val;
	}
	
	$child->{_PROXY} = $this->{_PROXY}->child() if $this->{_PROXY};
	$child->{_SOCKS5} = $this->{_SOCKS5}->child() if $this->{_SOCKS5};
	$child->{_SOCKS4} = $this->{_SOCKS4}->child() if $this->{_SOCKS4};
	$child->{_SOCKET} = $child->{$this->{_TYPE}};

	delete $child->{_CALLER};
	$child->{_PARRENT_SOCK} = 1;
	return $child;
}
sub type {$_[0]->{_SOCKET}->type()}
sub auth {
	my ($this, @prm) = @_;
	$this->{_SOCKET}->auth(@prm);
}
sub address {
	my ($this, $sckhost, $sckport) = @_;
	my ($cur_host, $cur_port) = $this->{_SOCKET}->address();
	
	if (defined $sckhost) {
		$this->release_sock($cur_host, $cur_port);
		($cur_host, $cur_port) = $this->{_SOCKET}->address($sckhost, $sckport);
	}
	return ($cur_host, $cur_port);
}
sub assign_sock {
	my ($this) = @_;
	my $type = $this->{_TYPES}->[int rand scalar @{$this->{_TYPES}}];
	$this->{_TYPE} = $type;
	$this->{_SOCKET} = $this->{$type};
	$this->{_POOL} = $this->{$type."_POOL"};
	
	$this->{_SOCKET}->address($this->{_POOL}->get);
}

sub release_sock {
	my ($this, $sckhost, $sckport) = @_;
	if (!$this->{_PARRENT_SOCK}) {
		$this->{_POOL}->free($sckhost, $sckport);
	} else {
		delete $this->{_PARRENT_SOCK};
	}
}

sub connect {
	my ($this, $host, $port, $caller) = @_;	
	$this->{_CALLER} = $caller;
	$this->{_HOST} = $host;
	$this->{_PORT} = $port;

	print "TRACE[".time."] ".$this->id." PROXY: CONNECT $this->{_TYPE} ".join(':',$this->address)."\n" if $TRACE;
	$this->{_SOCKET}->connect($host, $port, $this);
}

sub onconnect {
	my ($this, $wio, $wtm, $serv_ip, $serv_port) = @_;
	
	if (!$wio) {
		my ($msg, $code) = ($this->{_SOCKET}->errm, $this->{_SOCKET}->errc);
		$this->{_SOCKET}->close;
		
		# retry
		if ($this->{_REPEAT}--) {

			print "TRACE[".time."] ".$this->id." PROXY: ERROR   $this->{_TYPE} (".join(':',$this->address)."). left $this->{_REPEAT}. ($msg, $code)\n" if $TRACE;
			$this->release_sock;
			$this->assign_sock;

			print "TRACE[".time."] ".$this->id." PROXY: CONNECT $this->{_TYPE} ".join(':',$this->address)."\n" if $TRACE;
			$this->{_SOCKET}->connect($this->{_HOST}, $this->{_PORT}, $this);
			return;
		}
		
		$this->{_REPEAT} = $this->{_REPEAT_LIMIT};
		return $this->err($msg, $code);
	}
	
	$this->{_REPEAT} = $this->{_REPEAT_LIMIT};
	$this->callback($wio, $wtm, $serv_ip, $serv_port);
}

sub callback {
	my ($this, @resp) = @_;
	undef $this->{_ERRM};
	undef $this->{_ERRC};
	print "TRACE[".time."] ".$this->id." PROXY: DONE $this->{_TYPE} \n" if $TRACE;
	(delete $this->{_CALLER})->onconnect(@resp);
}

sub err {
	my ($this, $msg, $code) = @_;
	$this->{_ERRM} = $msg;
	$this->{_ERRC} = $code;

	print "TRACE[".time."] ".$this->id." PROXY: FAIL    $this->{_TYPE} (".join(':',$this->address)."). ($msg, $code)\n" if $TRACE;
	(delete $this->{_CALLER})->onconnect();
}

sub stop {
	my ($this) = @_;
	
	$this->release_sock;		
	$this->{_SOCKET}->stop;
	delete $this->{_CALLER};
}

sub repeat {
	my ($this, $val) = @_;
	$this->{_REPEAT_LIMIT} = $val if defined $val;
	return $this->{_REPEAT_LIMIT};
}

############ COMMON PROPIRTIES ##############
sub errc {$_[0]->{_ERRC}}
sub errm {$_[0]->{_ERRM}}

sub timeout {
	my ($this, $val) = @_;

	if (defined $val) {
		$this->{_HTTPPROXY}->timeout($val) if $this->{_HTTPPROXY};
		$this->{_PROXY}->timeout($val) if $this->{_PROXY};
		$this->{_SOCKS5}->timeout($val) if $this->{_SOCKS5};
		$this->{_SOCKS4}->timeout($val) if $this->{_SOCKS4};
	} else {
		$val = $this->{_SOCKET}->timeout();
	}
	return $val;
}

sub bindip {
	my ($this, $val) = @_;

	if (defined $val) {
		$this->{_HTTPPROXY}->bindip($val) if $this->{_HTTPPROXY};
		$this->{_PROXY}->bindip($val) if $this->{_PROXY};
		$this->{_SOCKS5}->bindip($val) if $this->{_SOCKS5};
		$this->{_SOCKS4}->bindip($val) if $this->{_SOCKS4};
	} else {
		$val = $this->{_SOCKET}->bindip();
	}
	return $val;
}

sub id {
	my ($this, $val) = @_;

	if (defined $val) {
		$this->{_HTTPPROXY}->id($val) if $this->{_HTTPPROXY};
		$this->{_PROXY}->id($val) if $this->{_PROXY};
		$this->{_SOCKS5}->id($val) if $this->{_SOCKS5};
		$this->{_SOCKS4}->id($val) if $this->{_SOCKS4};
	} else {
		$val = $this->{_SOCKET}->id();
	}
	return $val;
}


######## static methods #####
# return two related Proxy object for ftp command and data channels
sub ftp {
	my ($class, $socks5, $socks4, $proxy, $socket) =@_;
	my $cmd_sck = Net::Coro::Proxy->new( $socks5, $socks4, $proxy, undef, $socket);
	return if !$cmd_sck;
	
	my $data_sck = Net::Coro::Proxy::FTPData->new($cmd_sck);
	return ($cmd_sck, $data_sck);
}

# in common case the ftp data channel should use the same Proxy as command channel.
# that is why we should get actual proxy address every time we execute connect method.
package Net::Coro::Proxy::FTPData;
use common::sense;
use base 'Net::Coro::Proxy';
*TRACE = \$Net::Coro::Proxy::TRACE;
our $REPEAT = 2;
sub new {
	my ($class, $cmd_sck) = @_;
	my $this = bless {}, $class;
	$this->{_SOCKET} = $cmd_sck;
	$this->{_TYPE} = "DATA_CHANEL PROXY";
	
	$this->{_REPEAT_LIMIT} = $REPEAT;
	$this->{_REPEAT} = $this->{_REPEAT_LIMIT};
	return $this;
}

sub child {}

sub address {
	my ($this) = @_;
	$this->{_SOCKET}->address();
}

sub connect {
	my ($this, $host, $port, $caller) = @_;	
	$this->{_CALLER} = $caller;
	$this->{_HOST} = $host;
	$this->{_PORT} = $port;

	print "TRACE[".time."] ".$this->id." $this->{_TYPE}: connection begining - ".join(':',$this->address)."\n" if $TRACE;
	$this->{_SOCKET}->{_SOCKET}->connect($host, $port, $this);
}

sub onconnect {
	my ($this, $wio, $wtm, $serv_ip, $serv_port) = @_;
	
	if (!$wio) {
		my ($msg, $code) = ($this->{_SOCKET}->errm, $this->{_SOCKET}->errc);
		# retry
		if ($this->{_REPEAT}--) {
			$this->{_SOCKET}->{_SOCKET}->close;

			print "PROXY[".time."] ".$this->id." $this->{_TYPE}: (".join(':',$this->address).") fail. try left $this->{_REPEAT}. ($msg, $code)\n" if $TRACE;

			$this->{_SOCKET}->{_SOCKET}->connect($this->{_HOST}, $this->{_PORT}, $this);
			return;
		}
		
		$this->{_REPEAT} = $this->{_REPEAT_LIMIT};
		return $this->err($msg, $code);
	}
	
	$this->{_REPEAT} = $this->{_REPEAT_LIMIT};
	$this->callback($wio, $wtm, $serv_ip, $serv_port);
}

sub stop {
	my ($this) = @_;

	$this->{_SOCKET}->{_SOCKET}->stop;
	delete $this->{_CALLER};
}

sub timeout {$_[0]->{_SOCKET}->timeout()}
sub bindip {$_[0]->{_SOCKET}->bindip()}
sub id {$_[0]->{_SOCKET}->id()}


1;